import { File, type Store, type StoreState, compileFile } from '@vue/repl'
import { atou, utoa } from '@/utils/encode' // 处理字符
import { genCdnLink, genImportMap, genVueLink } from '@/utils/dependency'
import { IS_DEV } from '@/utils/constants'
import { type ImportMap, mergeImportMap } from '@/utils/import-map'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { UnwrapNestedRefs } from 'vue'
import mainCode from '@/example/basic/main.vue?raw'
import appCode from '@/example/basic/app.vue?raw'
import elementPlusCode from '@/example/basic/element-plus.js?raw'
import tsconfigCode from '@/example/basic/tsconfig.json?raw'

export interface Initial {
  serializedState?: string
  versions?: Versions
  userOptions?: UserOptions
  filesCode?: {
    [key: string]: string
  }
}
export type Versions = Record<VersionKey, string>

export type VersionKey = 'vue' | 'elementPlus' | 'typescript' | 'cesium'
export interface UserOptions {
  styleSource?: string
  showHidden?: boolean
}

export type SerializeState = Record<string, string> & {
  _o?: UserOptions
}

const MAIN_FILE = 'src/PlaygroundMain.vue' // 主文件
const APP_FILE = 'src/App.vue' // app文件
const ELEMENT_PLUS_FILE = 'src/element-plus.js'
const CESIUM_FILE = 'src/cesium.js'
const LEGACY_IMPORT_MAP = 'src/import_map.json'
export const TSCONFIG = 'tsconfig.json' // tsconfig
export const IMPORT_MAP = 'import-map.json' // import-map

export const useStore = (initial: Initial) => {
  const versions = reactive(
    initial.versions ||
      ({
        vue: 'latest',
        elementPlus: 'latest',
        typescript: 'latest',
        cesium: 'latest',
      } satisfies Versions),
  )
  const compiler = shallowRef<typeof import('vue/compiler-sfc')>()
  const userOptions = ref<UserOptions>(initial.userOptions || {})
  const hideFile = computed(() => !IS_DEV && !userOptions.value.showHidden)
  const _files = initFiles(initial.serializedState || '', initial.filesCode || {})

  let activeFile = _files[APP_FILE]
  if (!activeFile) activeFile = Object.values(_files)[0]

  // state
  const state: StoreState = reactive({
    mainFile: MAIN_FILE,
    files: _files,
    activeFile,
    errors: [],
    vueRuntimeURL: '',
    vueServerRendererURL: '',
    typescriptVersion: computed(() => versions.typescript),
    resetFlip: false,
    locale: undefined,
    dependencyVersion: computed(() => ({
      'element-plus': versions.elementPlus,
      cesium: versions.cesium,
    })),
  })
  const bultinImportMap = computed<ImportMap>(() => genImportMap(versions))
  const userImportMap = computed<ImportMap>(() => {
    const code = state.files[IMPORT_MAP]?.code.trim()
    if (!code) return {}
    let map: ImportMap = {}
    try {
      map = JSON.parse(code)
    } catch (error) {
      console.error(error)
    }
    return map
  })
  const importMap = computed<ImportMap>(() => {
    return mergeImportMap(bultinImportMap.value, userImportMap.value)
  })
  // store 示例
  const store = reactive<Store>({
    state,
    compiler: compiler as any,
    initialShowOutput: false,
    initialOutputMode: 'preview',
    init,
    setActive,
    addFile,
    deleteFile,
    getImportMap,
    renameFile,
    getTsConfig,
    reloadLanguageTools: undefined,
    customElement: false,
  })

  watch(
    () => versions.elementPlus,
    (version) => {
      const file = new File(
        ELEMENT_PLUS_FILE,
        generateElementPlusCode(version, userOptions.value.styleSource).trim(),
        hideFile.value,
      )
      state.files[ELEMENT_PLUS_FILE] = file
      compileFile(store, file).then((errs) => (state.errors = errs))
    },
    { immediate: true },
  )
  watch(
    () => versions.cesium,
    (version) => {
      const file = new File(
        CESIUM_FILE,
        generateCesiumCode(version, userOptions.value.styleSource).trim(),
        hideFile.value,
      )
      state.files[CESIUM_FILE] = file
      compileFile(store, file).then((errs) => (state.errors = errs))
    },
    { immediate: true },
  )
  function generateElementPlusCode(version: string, styleSource?: string) {
    const style = styleSource
      ? styleSource.replace('#VERSION#', version)
      : genCdnLink('element-plus', version, '/dist/index.css')
    return (initial.filesCode!.elementPlusCode || elementPlusCode).replace('#STYLE#', style)
  }
  function generateCesiumCode(version: string, styleSource?: string) {
    const style = styleSource
      ? styleSource.replace('#VERSION#', version)
      : genCdnLink('cesium', version, '/Build/Cesium/Widgets/widgets.css')
    const script = genCdnLink('cesium', version, '/Build/Cesium/Cesium.js')

    return initial.filesCode!.cesiumCode.replace('#SCRIPT#', script).replace('#STYLE#', style)
  }
  // 设置vue版本
  async function setVueVersion(version: string) {
    const { compilerSfc, runtimeDom } = genVueLink(version)

    compiler.value = await import(/* @vite-ignore */ compilerSfc)
    state.vueRuntimeURL = runtimeDom
    versions.vue = version

    // eslint-disable-next-line no-console
    console.info(`[@vue/repl] Now using Vue version: ${version}`)
  }
  // 初始化方法
  let inited = false
  async function init() {
    if (inited) return

    await setVueVersion(versions.vue)

    state.errors = []
    for (const file of Object.values(state.files)) {
      compileFile(store, file).then((errs) => state.errors.push(...errs))
    }

    watchEffect(() => compileFile(store, state.activeFile).then((errs) => (state.errors = errs)))

    watch(
      () => [
        state.files[TSCONFIG]?.code,
        state.typescriptVersion,
        state.locale,
        state.dependencyVersion,
      ],
      useDebounceFn(() => store.reloadLanguageTools?.(), 300),
      { deep: true },
    )

    inited = true
  }
  function getFiles() {
    const exported: Record<string, string> = {}
    for (const file of Object.values(state.files)) {
      if (file.hidden) continue
      exported[file.filename] = file.code
    }
    return exported
  }

  function serialize() {
    const state: SerializeState = { ...getFiles() }
    state._o = userOptions.value
    return utoa(JSON.stringify(state))
  }

  function deserialize(text: string): SerializeState {
    const state = JSON.parse(atou(text))
    return state
  }
  /**
   * @title: 初始化文件
   * @param {string} serializedState 是否已保存文件，是则解码
   * @return {*}
   */
  function initFiles(serializedState: string, filesCode: Initial['filesCode']) {
    const files: StoreState['files'] = {}
    if (serializedState) {
      const saved = deserialize(serializedState)
      /* eslint-disable-next-line */
      for (let [filename, file] of Object.entries(saved)) {
        if (filename === '_o') continue
        if (![IMPORT_MAP, TSCONFIG].includes(filename) && !filename.startsWith('src/')) {
          filename = `src/${filename}`
        }
        if (filename === LEGACY_IMPORT_MAP) {
          filename = IMPORT_MAP
        }
        files[filename] = new File(filename, file as string)
      }
      userOptions.value = saved._o || {}
    } else {
      files[APP_FILE] = new File(APP_FILE, filesCode!.appCode || appCode)
    }
    files[MAIN_FILE] = new File(MAIN_FILE, filesCode!.mainCode || mainCode, hideFile.value)
    if (!files[IMPORT_MAP]) {
      files[IMPORT_MAP] = new File(IMPORT_MAP, JSON.stringify({ imports: {} }, undefined, 2))
    }
    if (!files[TSCONFIG]) {
      files[TSCONFIG] = new File(TSCONFIG, filesCode!.tsconfigCode || tsconfigCode)
    }
    return files
  }

  // 当前活跃文件
  function setActive(filename: string) {
    const file = state.files[filename]
    if (file.hidden) return
    state.activeFile = state.files[filename]
  }
  // 添加文件
  function addFile(fileOrFilename: string | File) {
    const file = typeof fileOrFilename === 'string' ? new File(fileOrFilename) : fileOrFilename
    state.files[file.filename] = file
    setActive(file.filename)
  }
  // 删除文件
  async function deleteFile(filename: string) {
    if ([ELEMENT_PLUS_FILE, MAIN_FILE, APP_FILE, CESIUM_FILE, IMPORT_MAP].includes(filename)) {
      ElMessage.warning('你不能删除它，因为项目需要它.')
      return
    }

    if (
      await ElMessageBox.confirm(`您确定要删除吗 ${filename.replace(/^src\//, '')}?`, {
        title: 'Delete File',
        type: 'warning',
        center: true,
      })
    ) {
      if (state.activeFile.filename === filename) {
        setActive(APP_FILE)
      }
      // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
      delete state.files[filename]
    }
  }
  // importMap 对象
  function getImportMap() {
    return importMap.value
  }
  // 重命名
  function renameFile(oldFilename: string, newFilename: string) {
    const file = state.files[oldFilename]

    if (!file) {
      state.errors = [`Could not rename "${oldFilename}", file not found`]
      return
    }

    if (!newFilename || oldFilename === newFilename) {
      state.errors = [`Cannot rename "${oldFilename}" to "${newFilename}"`]
      return
    }

    if (
      file.hidden ||
      [APP_FILE, MAIN_FILE, ELEMENT_PLUS_FILE, CESIUM_FILE, IMPORT_MAP].includes(oldFilename)
    ) {
      state.errors = [`Cannot rename ${oldFilename}`]
      return
    }

    file.filename = newFilename

    const newFiles: Record<string, File> = {}

    // Preserve iteration order for files
    for (const name of Object.keys(_files)) {
      if (name === oldFilename) {
        newFiles[newFilename] = file
      } else {
        newFiles[name] = _files[name]
      }
    }

    state.files = newFiles
    compileFile(store, file)
  }
  // tsconfig
  function getTsConfig() {
    try {
      return JSON.parse(state.files[TSCONFIG].code)
    } catch {
      return {}
    }
  }
  async function setVersion(key: VersionKey, version: string) {
    switch (key) {
      case 'vue':
        await setVueVersion(version)
        break
      case 'elementPlus':
        versions.elementPlus = version
        break
      case 'typescript':
        versions.typescript = version
        break
      case 'cesium':
        versions.cesium = version
        break
    }
  }
  const utils = {
    versions,
    userOptions,
    serialize,
    setVersion,
  }
  Object.assign(store, utils)
  return store as Omit<typeof store, 'init'> & {
    init: typeof init
  } & UnwrapNestedRefs<typeof utils>
}

export type ReplStore = ReturnType<typeof useStore>
